
/* GCSx
** DIALOG.CPP
**
** Dialog boxes
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

// Dialog class

Dialog::Dialog(const string& dTitle, int dAutoApply, int dHasTitlebar, int toolPanel) : WindowCollection(), Window(), title(dTitle) { start_func
    isToolPanel = toolPanel;
    hasTitlebar = dHasTitlebar;
    lastButtonId = 0;
    open = 0;
    openModal = 0;
    nextTabId = 0;
    noAutoClose = 0;
    autoApply = dAutoApply;
    myFrame = NULL;
    lastWidgetArranged = -1;
    nextWidgetY = (toolPanel ? GUI_PANEL_TOPMARGIN : GUI_DIALOG_TOPMARGIN);
    inSiblingModify = 0;
}

Dialog::~Dialog() { start_func
    // (shall already have closed off of desktop)
    // Delete all widgets that wish to be
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        // If this were called during the wrong exception, windowDataDisplay COULD have NULLs in it
        if (*pos) {
            if ((*pos)->wantsToBeDeleted()) delete *pos;
        }
    }
}

void Dialog::setTitle(const string& newTitle) { start_func
    title = newTitle;
    if (myFrame) myFrame->setTitle(newTitle);
}

void Dialog::clearWidgets() { start_func
    assert(!open);

    // Delete all widgets that wish to be
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        // If this were called during the wrong exception, windowDataDisplay COULD have NULLs in it
        if (*pos) {
            if ((*pos)->wantsToBeDeleted()) delete *pos;
        }
    }
    
    windowDataDisplay.clear();
    windowDataFocus.clear();

    currentFocus = NULL;
    previousFocus = NULL;
    lastFocus = NULL;
    currentMouseFocus = NULL;
    modalPos = NULL;

    nextTabId = 0;
    lastWidgetArranged = -1;
    nextWidgetY = (isToolPanel ? GUI_PANEL_TOPMARGIN : GUI_DIALOG_TOPMARGIN);
    
    resize(1, 1);
}

#ifndef NDEBUG
const char* Dialog::debugDump() const { start_func
    // return title.c_str();
    return "DIALOG";
}
#endif

int Dialog::event(int hasFocus, const SDL_Event* event) { start_func
    assert(event);

    // Check for SDL_CLOSE events- this means we can close safely, frame window is closing
    if (event->type == SDL_CLOSE) {
        open = 0;
        openModal = 0;
        myFrame = NULL;
        
        // Frame window will delete us if we've requested being deleted, so we're done!
        return 1;
    }
    
    // We don't propogate these events (they'd infinite loop)
    // but we need to send them to the currently active element
    if (event->type == SDL_INPUTFOCUS) {
        if (event->user.code & 1) {
            changeInputFocus(lastFocus);
            lastFocus = NULL;
        }
        else {
            if (!lastFocus) {
                lastFocus = currentFocus;
                changeInputFocus(NULL);
            }
        }
        return 1;
    }
    else if (event->type == SDL_MOUSEFOCUS) {
        if (!(event->user.code & 1) && (currentMouseFocus)) {
            int result = windowRunEvent(currentMouseFocus, event);
            currentMouseFocus = NULL;
            return result;
        }
        return 0;
    }

    int result = handleEvent(event);
    
    // Handle special "global" keys like TAB
    if ((!result) && (event->type == SDL_KEYDOWN)) {
        int id = -1;
        
        switch (combineKey(event->key.keysym.sym, event->key.keysym.mod)) {
            case SDLK_TAB:
                // Next widget
                return nextControl();
                
            case combineKey(SDLK_TAB, KMOD_SHIFT):
                // Previous widget
                return prevControl();
                    
            case SDLK_DOWN:
            case SDLK_RIGHT:
                // Next widget with same id
                if (currentFocus) id = dynamic_cast<Widget*>(currentFocus)->getId();
                return nextControl(id);
                    
            case SDLK_UP:
            case SDLK_LEFT:
                // Previous widget with same id
                if (currentFocus) id = dynamic_cast<Widget*>(currentFocus)->getId();
                return prevControl(id);
                
            case SDLK_KP_ENTER:
            case SDLK_RETURN:
                if (noAutoClose) break;

                // Current button, or OK button if none
                if (currentFocus) {
                    if (typeid(*currentFocus) == typeid(WButton)) {
                        dynamic_cast<Widget*>(currentFocus)->doAction();
                        return 1;
                    }
                }
                // (find OK button)
                if (buttonControl(BUTTON_OK)) {
                    dynamic_cast<Widget*>(currentFocus)->doAction();
                }
                return 1;

            case SDLK_ESCAPE:
                if (noAutoClose) break;

                // (find CANCEL button?) 
                if (buttonControl(BUTTON_CANCEL)) {
                    dynamic_cast<Widget*>(currentFocus)->doAction();
                }
                else {
                    desktop->setModalReturn(0);
                    closeWindow(); // Calls attemptClose
                }
                return 1;
                
            default:
                // Plain or ALT only
                if ((event->key.keysym.mod == KMOD_ALT) ||(event->key.keysym.mod == KMOD_NONE)) {
                    // Hotkey?
                    if (hotkeyControl(event->key.keysym.sym)) return 1;
                }
                break;
        }
    }
    
    return result;
}

void Dialog::currentFocusDoAction() { start_func
    if (currentFocus) {
        dynamic_cast<Widget*>(currentFocus)->doAction();
    }
}

void Dialog::resolutionChange(int fromW, int fromH, int fromBpp, int toW, int toH, int toBpp) { start_func
    Window::resolutionChange(fromW, fromH, fromBpp, toW, toH, toBpp);
    WindowCollection::resolutionChange(fromW, fromH, fromBpp, toW, toH, toBpp);
}

void Dialog::display(SDL_Surface* destSurface, Rect& toDisplay, const Rect& clipArea, int xOffset, int yOffset) { start_func
    assert(destSurface);

    if (visible) {
        // If dirty, redraw all
        if (dirty) {
            getRect(toDisplay);
            toDisplay.x += xOffset;
            toDisplay.y += yOffset;
            dirty = 0;
            updateRect.w = 0;
        }
        // Redraw portion requested?
        else if (updateRect.w) {
            updateRect.x += x + xOffset;
            updateRect.y += y + yOffset;
            boundRects(toDisplay, updateRect);
            updateRect.w = 0;
        }
        
        intersectRects(toDisplay, clipArea);
        
        // Anything to draw?
        if ((toDisplay.w) || (childDirty)) {
            // Fill with background
            if (toDisplay.w) {
                SDL_FillRect(destSurface, &toDisplay, guiPacked[COLOR_FILL]);
            }

            xOffset += x;
            yOffset += y;

            list<Window*>::iterator end = windowDataDisplay.end();
            for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
                assert(*pos);
                // Start with clip area of entire widget
                Rect clip;
                (*pos)->getRect(clip);
                clip.x += xOffset;
                clip.y += yOffset;
                // If this intersects with our current update area OR widget is dirty
                if ((intersectRects(clip, toDisplay)) || ((*pos)->isDirty())) {
                    // ...then display widget (we only request area that matches our update)
                    (*pos)->display(destSurface, clip, clipArea, xOffset, yOffset);
                    // and add in anything it did update to our update area
                    boundRects(toDisplay, clip);
                }
            }
            
            childDirty = 0;
        }
    }
}

const char* Dialog::tooltip(int xPos, int yPos) const { start_func
    const Window* which = findWindowByCoords(xPos, yPos);
    if (which) {
        return which->tooltip(xPos - which->getX(), yPos - which->getY());
    }
    return NULL;
}

int Dialog::attemptClose() { start_func
    // If we know we're already closing...
    if ((!open) && (!openModal)) {
        // We're ok!
        return 1;
    }

    ButtonAction result = verifyEntry(0, BUTTON_CANCEL);
    
    // Action to take- closing is a cancel by default
    if (result == BUTTON_DEFAULT) result = BUTTON_CANCEL;

    // We can't call doAction because that calls closeWindow...
    if ((result == BUTTON_OK) || (result == BUTTON_APPLY)) {
        applySettings();
    }                    

    if ((result == BUTTON_OK) || (result == BUTTON_CANCEL)) {
        // We want to close
        return 1;
    }
    
    // We don't want to close
    return 0;
}

int Dialog::doAction(ButtonAction buttonType) { start_func
    if ((buttonType == BUTTON_OK) || (buttonType == BUTTON_APPLY)) {
        applySettings();
    }                    
    if ((buttonType == BUTTON_OK) || (buttonType == BUTTON_CANCEL)) {
        desktop->setModalReturn(0); // Defaults to 0 return, if not overridden
        closeWindow(1);
        return 1;
    }            
    
    return 0;        
}

Dialog::ButtonAction Dialog::verifyEntry(int buttonId, ButtonAction buttonType) { start_func
    if ((buttonType == BUTTON_APPLY) || (buttonType == BUTTON_OK)) {
        list<Window*>::iterator end = windowDataDisplay.end();
        for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
            assert(*pos);
            Widget* curr = dynamic_cast<Widget*>(*pos);
            if (curr->refuseAll()) continue;
            if (!curr->entryValid()) {
                bringToTop(curr);
                curr->tabAction();
                return BUTTON_NOTHING;
            }
        }
    }

    // (you must derive a new Dialog class to do anything here)
    return BUTTON_DEFAULT;
}
    
void Dialog::addWidget(Widget* newWidget) { start_func
    addWindow(newWidget, 0);
    newWidget->stateTabId(++nextTabId);
}

void Dialog::arrangeRow(int vertCenter, int leftColumnSize, int staticRight) { start_func
    list<Window*>::iterator end = windowDataDisplay.end();
    list<Window*>::iterator pos;
    int count = 0;
    for (pos = windowDataDisplay.begin(); (pos != end) && (count <= lastWidgetArranged); ++pos, ++count) ;
    
    // pos contains next widget to arrange
    if (pos == end) return;
    
    int tallest = 0;
    if (vertCenter) {
        // Determine tallest now, for centering
        for (; pos != end; ++pos) {
            if ((*pos)->getHeight() > tallest) tallest = (*pos)->getHeight();
        }

        // Start at beginning again
        int count = 0;
        for (pos = windowDataDisplay.begin(); (pos != end) && (count <= lastWidgetArranged); ++pos, ++count) ;
    }
    
    // Arrange in a row
    int widgetX = (isToolPanel ? GUI_PANEL_LEFTMARGIN : GUI_DIALOG_LEFTMARGIN);
    int totalWidth = -GUI_DIALOG_SEPWIDTH; // (because first seperation doesn't count)
    int first = 1;
    for (; pos != end; ++count, ++pos) {
        int wide = leftColumnSize;
        int rightAlign = 0;
        if ((!first) || (wide < 0)) {
            wide = (*pos)->getWidth();
        }
        // Right align?
        if ((staticRight) && (typeid(**pos) == typeid(WStatic))) {
            rightAlign = wide - (*pos)->getWidth();
        }
        
        if (vertCenter) {
            (*pos)->move(widgetX + rightAlign, nextWidgetY + (tallest - (*pos)->getHeight()) / 2);
        }
        else {
            (*pos)->move(widgetX + rightAlign, nextWidgetY);
            if ((*pos)->getHeight() > tallest) tallest = (*pos)->getHeight();
        }
        widgetX += wide + GUI_DIALOG_SEPWIDTH;
        totalWidth += wide + GUI_DIALOG_SEPWIDTH;
        first = 0;
    }
    
    // Next row goes where?
    nextWidgetY += tallest + GUI_DIALOG_SEPHEIGHT;
    
    // Resize dialog width?
    totalWidth += (isToolPanel ? (GUI_PANEL_LEFTMARGIN + GUI_PANEL_RIGHTMARGIN) : (GUI_DIALOG_LEFTMARGIN + GUI_DIALOG_RIGHTMARGIN));
    if (totalWidth < width) totalWidth = width;
    
    // Resize dialog, ensure we're dirty
    resize(totalWidth, nextWidgetY - GUI_DIALOG_SEPHEIGHT + (isToolPanel ? GUI_PANEL_BOTTOMMARGIN : GUI_DIALOG_BOTTOMMARGIN));
    setDirty(1);
    
    // Mark our spot for next time
    lastWidgetArranged = count - 1;
}

void Dialog::makePretty(int maxColumns, int vertCenter, int pureFlow, int staticRight) { start_func
    assert(maxColumns >= 1);
    assert(maxColumns <= GUI_DIALOG_NUMCOL);

    // Find width of largest widget in each column; sum up heights
    // Find tallest for use with vertical cemtering
    int widest[GUI_DIALOG_NUMCOL];
    int tallest[GUI_DIALOG_NUMROW];
    int currRow = 0;
    int totalHeight = nextWidgetY;
    // Of bottom row
    int totalButtonWidth = 0;
    int maxButtonHeight = 0;
    Window* firstButton = NULL;
    list<Window*>::iterator firstButtonPos;
    int skipButtons = 0;
    int prevColumn = -1;
    int rowMaxHeight = 0;
    int rowMaxSep = 0;
    
    // (pre-define column widests/tallests)
    for (int col = 0; col < GUI_DIALOG_NUMCOL; ++col) {
        widest[col] = 0;
    }
    for (int row = 0; row < GUI_DIALOG_NUMROW; ++row) {
        tallest[row] = 0;
    }

    list<Window*>::iterator end = windowDataDisplay.end();
    list<Window*>::iterator pos;
    int count = 0;
    for (pos = windowDataDisplay.begin(); (pos != end) && (count <= lastWidgetArranged); ++pos, ++count) ;
    
    // pos contains next widget to arrange
    if (pos == end) return;

    list<Window*>::iterator next;
    for (; pos != end; pos = next) {
        assert(*pos);
        next = pos;
        ++next;
        
        // Calculate a row of buttons
        if (typeid(**pos) == typeid(WButton)) {
            if (!skipButtons) {
                if (!firstButton) {
                    firstButtonPos = pos;
                    firstButton = *pos;
                }
                else {
                    totalButtonWidth += GUI_DIALOG_SEPWIDTH;
                }
                
                if ((*pos)->getHeight() > maxButtonHeight) maxButtonHeight = (*pos)->getHeight();
                totalButtonWidth += (*pos)->getWidth();
                continue;
            }
        }
        // If not a button but we were doing a row of buttons, redo that row
        // as normal controls
        else if (firstButton) {
            firstButton = NULL;
            totalButtonWidth = 0;
            maxButtonHeight = 0;
            pos = firstButtonPos;
            --pos;
            skipButtons = 1;
            continue;
        }
        else {
            skipButtons = 0;
        }
        
        // Which column?
        int column = prevColumn + 1;
        // Pure flow- columns in order, period
        if (pureFlow) {
            if (column >= maxColumns) column = 0;
        }
        // Otherwise...
        else {
            // First time, we start in 2nd column
            if (prevColumn == -1) column = 1;
            // If we're past the column limit, reset to
            // 2nd column again, unless we've requested 1 column only
            if (column >= maxColumns) {
                if (maxColumns == 1) column = 0;
                else column = 1;
            }
            // Static labels always go in 1st column
            if (typeid(**pos) == typeid(WStatic)) {
                column = 0;
            }
        }
        
        // If same or less column, finish most recent row
        if (column <= prevColumn) {
            assert(currRow < GUI_DIALOG_NUMROW);
            tallest[currRow] = rowMaxHeight;
            totalHeight += rowMaxHeight + rowMaxSep;
            rowMaxHeight = 0;
            rowMaxSep = 0;
            ++currRow;
        }
        prevColumn = column;
        
        // Determine highest item in this row
        if ((*pos)->getHeight() > rowMaxHeight) rowMaxHeight = (*pos)->getHeight();
        
        // Determine separator below this row
        int sep;
        if (next == end) sep = 0;
        else if ((typeid(**pos) == typeid(*next)) &&
                 ((typeid(*next) == typeid(WRadioButton)) ||
                  (typeid(*next) == typeid(WCheckBox)))) sep = GUI_DIALOG_CHECKSEPHEIGHT;
        else sep = GUI_DIALOG_SEPHEIGHT;
        if (sep > rowMaxSep) rowMaxSep = sep;
        
        // Determine widest item in each column
        if ((*pos)->getWidth() > widest[column]) {
            widest[column] = (*pos)->getWidth();
        }
    }

    // Finish last row
    assert(currRow < GUI_DIALOG_NUMROW);
    tallest[currRow] = rowMaxHeight;
    totalHeight += rowMaxHeight;
    
    // Did we end up with a button row?
    if (firstButton) totalHeight += GUI_DIALOG_BUTTONSEPHEIGHT + maxButtonHeight;
    
    // Resize ourselves
    int colSum = widest[GUI_DIALOG_NUMCOL - 1];
    for (int col = 0; col < (GUI_DIALOG_NUMCOL - 1); ++col) {
        if ((widest[col]) && (widest[col + 1])) colSum += GUI_DIALOG_GUTTERWIDTH;
        colSum += widest[col];
    }
    if (totalButtonWidth > colSum) colSum = totalButtonWidth;

    // Resize dialog width?
    int totalWidth = colSum + (isToolPanel ? (GUI_PANEL_LEFTMARGIN + GUI_PANEL_RIGHTMARGIN) : (GUI_DIALOG_LEFTMARGIN + GUI_DIALOG_RIGHTMARGIN));
    if (totalWidth < width) totalWidth = width;
    
    // Resize dialog, ensure we're dirty
    resize(totalWidth, totalHeight + (isToolPanel ? GUI_PANEL_BOTTOMMARGIN : GUI_DIALOG_BOTTOMMARGIN));
    setDirty(1);

    // Restart
    count = 0;
    for (pos = windowDataDisplay.begin(); (pos != end) && (count <= lastWidgetArranged); ++pos, ++count) ;
    // pos contains next widget to arrange again
    
    // Place elements, starting at upper left margin
    int x;
    int y = nextWidgetY;
    int buttonMode = 0;
    int yCenter = 0;
    prevColumn = -1;
    rowMaxHeight = 0;
    rowMaxSep = 0;
    currRow = 0;
    for (; pos != end; pos = next, ++count) {
        assert(*pos);
        next = pos;
        ++next;
        
        if (*pos == firstButton) {
            buttonMode = 1;
            x = (width - totalButtonWidth) / 2;
            // Add separation and height from previous row (but not previous row sep)
            y += tallest[currRow] + GUI_DIALOG_BUTTONSEPHEIGHT;
            ++currRow;
        }
        
        if (buttonMode) {
            (*pos)->move(x, y);
            x += (*pos)->getWidth() + GUI_DIALOG_SEPWIDTH;
        }
        else {
            // Which column?
            int column = prevColumn + 1;
            int alignRight = 0;
            if (pureFlow) {
                if (column >= maxColumns) column = 0;
                if ((staticRight) && (typeid(**pos) == typeid(WStatic))) alignRight = 1;
            }
            else {
                if (prevColumn == -1) column = 1;
                if (column >= maxColumns) {
                    if (maxColumns == 1) column = 0;
                    else column = 1;
                }
                if (typeid(**pos) == typeid(WStatic)) {
                    column = 0;
                    alignRight = 1;
                }
            }

            // If same or less column, finish most recent row
            if (column <= prevColumn) {
                y += tallest[currRow] + rowMaxSep;
                ++currRow;
                rowMaxSep = 0;
            }
            prevColumn = column;
            
            // Determine separator below this row
            int sep;
            if (next == end) sep = 0;
            else if ((typeid(**pos) == typeid(*next)) &&
                     ((typeid(*next) == typeid(WRadioButton)) ||
                      (typeid(*next) == typeid(WCheckBox)))) sep = GUI_DIALOG_CHECKSEPHEIGHT;
            else sep = GUI_DIALOG_SEPHEIGHT;
            if (sep > rowMaxSep) rowMaxSep = sep;
            
            // Determine column x
            x = (isToolPanel ? GUI_PANEL_LEFTMARGIN : GUI_DIALOG_LEFTMARGIN);
            for (int col = 0; col < column; ++col) {
                if ((col < (GUI_DIALOG_NUMCOL - 1)) && (widest[col]) && (widest[col + 1])) x += GUI_DIALOG_GUTTERWIDTH;
                x += widest[col];
            }
            
            // Right align?
            if (alignRight) {
                x += widest[column] - (*pos)->getWidth();
            }
            
            // Vertically center?
            if (vertCenter) {
                yCenter = (tallest[currRow] - (*pos)->getHeight()) / 2;
            }
            
            (*pos)->move(x, y + yCenter);
        }
    }
    
    // Mark our spot for next time
    nextWidgetY = totalHeight + GUI_DIALOG_SEPHEIGHT;
    lastWidgetArranged = count - 1;
}
    
int Dialog::runModal(int xPos, int yPos) { start_func
    // Prevent duplication
    if (myFrame) return 0;

    // Keep a backup cause we clear myFrame on SDL_CLOSE
    FrameWindow* myFrameBackup;
    myFrameBackup = new FrameWindow(title, FrameWindow::RESIZING_OFF, FrameWindow::FRAMETYPE_DIALOG, this, hasTitlebar ? FrameWindow::TITLEBAR_NORMAL : FrameWindow::TITLEBAR_OFF);
    myFrame = myFrameBackup;
    // Frame window can NOT delete itself- we want to do that, to ensure it isn't
    // deleted twice on exception
    myFrameBackup->setWantsToBeDeleted(0);
    
    lastButtonId = 0;
    // We have to set this before showing the window, so the desktop knows it's modal
    openModal = 1;
    open = 1;
    
    loadSettings();
    
    // Modal dialogs default to center of SCREEN, not desktop
    if (xPos == -1) {
        xPos = (screenWidth - myFrameBackup->getWidth()) / 2;
        if (xPos < 0) xPos = 0;
    }
    if (yPos == -1) {
        yPos = (screenHeight - myFrameBackup->getHeight()) / 2;
        if (yPos < 0) yPos = 0;
    }
    
    myFrameBackup->show(xPos, yPos, FrameWindow::SHOW_CURRENT, FrameWindow::SHOW_CURRENT);
    firstControl();
    // So we don't deselect it immediately
    lastFocus = currentFocus;
    // Recurse into desktop event loop
    int returnValue = desktop->eventLoop();
    // Delete frame
    delete myFrameBackup;
    myFrame = NULL;
    return returnValue;
}

void Dialog::firstControl() { start_func
    changeInputFocus(NULL);
    nextControl();
}

void Dialog::runAsPanel() { start_func
    open = 1;
    noAutoClose = 1;
    loadSettings();

    // Select first control
    firstControl();

    // Deselect it immediately, now we know the proper "first" control
    lastFocus = currentFocus;
    changeInputFocus(NULL);
}
    
void Dialog::runWindowed(int xPos, int yPos) { start_func
    // Prevent duplication
    if (myFrame) {
        desktop->bringToTop(myFrame);
        return;
    }

    // We remember the frame pointer even though it'll delete itself
    myFrame = new FrameWindow(title, FrameWindow::RESIZING_OFF, FrameWindow::FRAMETYPE_DIALOG, this, hasTitlebar ? FrameWindow::TITLEBAR_NORMAL : FrameWindow::TITLEBAR_OFF);
    lastButtonId = 0;
    open = 1;
    
    loadSettings();

    // Non-modal dialogs go to center of DESKTOP, not entire screen
    if (xPos == -1) {
        xPos = desktop->desktopX() + (desktop->desktopWidth() - myFrame->getWidth()) / 2;
        if (xPos < 0) xPos = 0;
    }
    if (yPos == -1) {
        yPos = desktop->desktopY() + (desktop->desktopHeight() - myFrame->getHeight()) / 2;
        if (yPos < 0) yPos = 0;
    }

    myFrame->show(xPos, yPos, FrameWindow::SHOW_CURRENT, FrameWindow::SHOW_CURRENT);
    firstControl();
    // So we don't deselect it immediately
    lastFocus = currentFocus;
}
    
Widget* Dialog::findWidget(int id, int which) { start_func
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        assert(*pos);
        if (dynamic_cast<Widget*>(*pos)->getId() == id) {
            if (which) --which;
            else return dynamic_cast<Widget*>(*pos)->returnSelf();
        }
    }
    return NULL;
}

void Dialog::applySettings() { start_func
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        assert(*pos);
        dynamic_cast<Widget*>(*pos)->apply();
    }
}

void Dialog::loadSettings() { start_func
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        assert(*pos);
        dynamic_cast<Widget*>(*pos)->load();
    }
}

Window::WindowSort Dialog::windowSort() const { start_func
    if (openModal) return WINDOWSORT_MODAL;
    return WINDOWSORT_NORMAL;
}

Window::WindowType Dialog::windowType() const { start_func
    return WINDOW_CLIENT;
}

int Dialog::wantsToBeDeleted() const { start_func
    return 0;
}

Window::CommandSupport Dialog::supportsCommand(int code) const { start_func
    if (currentFocus) {
        return currentFocus->supportsCommand(code);
    }
    return Window::COMMAND_HIDE;
}

int Dialog::hotkeyControl(char keypress) { start_func
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        assert(*pos);
        Widget* curr = dynamic_cast<Widget*>(*pos);
        if (curr->getShortcut() == keypress) {
            int refuse = curr->refuseAll();
            if (refuse == WStatic::REFUSE_STATIC) {
                // Select next control, but no action
                nextControl(-1, curr);
                return 1;
            }
            if (refuse) {
                return 0;
            }
            bringToTop(curr);
            curr->doAction();
            return 1;
        }
    }
    
    return 0;
}

int Dialog::buttonControl(ButtonAction action) { start_func
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        assert(*pos);
        if (typeid(**pos) == typeid(WButton)) {
            if (dynamic_cast<Widget*>(*pos)->refuseAll()) continue;
            if (dynamic_cast<WButton*>(*pos)->getAction() == action) {
                bringToTop(dynamic_cast<Widget*>(*pos));
                return 1;
            }
        }
    }
    
    return 0;
}

int Dialog::nextControl(int id, const Widget* from) { start_func
    // Current widget tab id
    int currId = 0;
    if (from) currId = from->stateTabId();
    else if (currentFocus) currId = dynamic_cast<Widget*>(currentFocus)->stateTabId();
    
    // Search for the closest higher id; also record lowest id
    int closestId = 0x7FFF;
    Widget* closestWidget = NULL;
    int lowestId = 0x7FFF;
    Widget* lowestWidget = NULL;
    
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        assert(*pos);
        Widget* curr = dynamic_cast<Widget*>(*pos);
        int tabId = curr->stateTabId();
        if ((id >= 0) && (curr->getId() != id)) continue;
        if (curr->refuseAll()) continue;
        if ((tabId < closestId) && (tabId > currId)) {
            closestId = tabId;
            closestWidget = curr;
        }
        if (tabId < lowestId) {
            lowestId = tabId;
            lowestWidget = curr;
        }
    }
    
    if (!closestWidget) closestWidget = lowestWidget;
    if ((closestWidget) && (currentFocus != closestWidget)) {
        bringToTop(closestWidget);
        closestWidget->tabAction();
        return 1;
    }
    
    return 0;
}

int Dialog::prevControl(int id) { start_func
    // Current widget id
    int currId = 0;
    if (currentFocus) currId = dynamic_cast<Widget*>(currentFocus)->stateTabId();
    
    // Search for the closest lower id; also record highest id
    int closestId = 0;
    Widget* closestWidget = NULL;
    int highestId = 0;
    Widget* highestWidget = NULL;
    
    list<Window*>::iterator end = windowDataDisplay.end();
    for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
        assert(*pos);
        Widget* curr = dynamic_cast<Widget*>(*pos);
        int tabId = curr->stateTabId();
        if ((id >= 0) && (curr->getId() != id)) continue;
        if (curr->refuseAll()) continue;
        if ((tabId > closestId) && (tabId < currId)) {
            closestId = tabId;
            closestWidget = curr;
        }
        if (tabId > highestId) {
            highestId = tabId;
            highestWidget = curr;
        }
    }
    
    if (!closestWidget) closestWidget = highestWidget;
    if ((closestWidget) && (currentFocus != closestWidget)) {
        bringToTop(closestWidget);
        closestWidget->tabAction();
        return 1;
    }
    
    return 0;
}

void Dialog::childModified(Window* modified) { start_func
    assert(modified);
    Widget* widget = dynamic_cast<Widget*>(modified);
    
    if (widget) {
        if (autoApply) widget->apply();

        // Don't recurse modifies
        if (inSiblingModify) return;
        inSiblingModify = 1;

        // Tell all other widgets
        list<Window*>::iterator end = windowDataDisplay.end();
        for (list<Window*>::iterator pos = windowDataDisplay.begin(); pos != end; ++pos) {
            if (*pos != modified) {
                Widget* lWidget = dynamic_cast<Widget*>(*pos);
                if (lWidget) lWidget->siblingModified(widget);
            }
        }

       inSiblingModify = 0;
    }
    
    Window::childModified(modified);
}

// Widget class

Widget::Widget(int wId, const string& wLabel, void* wSetting) : Window(), label() { start_func
    assert(wId >= 0);
    
    setLabel(wLabel);

    disabled = 0;
    haveFocus = 0;
    id = wId;
    setting = wSetting;
    tabId = -1;
    tip = NULL;
}

Widget::~Widget() { start_func
}

void Widget::setLabel(const string& newLabel) { start_func
    label = newLabel;
    if (label.length() > MAX_LINELENGTH) label = label.substr(0, MAX_LINELENGTH);
    underline = convertGuiText(&label, &shortcut);
}

void Widget::setToolTip(const char* wTip) { start_func
    assert(wTip);
    
    tip = wTip;
}

const char* Widget::tooltip(int xPos, int yPos) const { start_func
    return tip;
}

Widget* Widget::returnSelf() { start_func
    return this;
}

void Widget::changeStorage(void* newSetting) { start_func
    setting = newSetting;
}

void Widget::addTo(Dialog* dialog) { start_func
    dialog->addWidget(this);
}

#ifndef NDEBUG
const char* Widget::debugDump() const { start_func
    if (label.size()) return label.c_str();
    return "(widget)";
}
#endif

int Widget::stateTabId(int wTabId) { start_func
    assert(wTabId > 0);
    tabId = wTabId;
    return tabId;
}

int Widget::stateTabId() const { start_func
    assert(tabId > 0);
    return tabId;
}

int Widget::refuseAll() const { start_func
    if (disabled) return 1;
    return 0;
}
    
void Widget::siblingModified(Widget* modified) { start_func
    assert(modified);
    assert(modified != this);
}

int Widget::entryValid() const { start_func
    return 1;
}

void Widget::doAction() { start_func
}

void Widget::tabAction() { start_func
}

int Widget::wantsToBeDeleted() const { start_func
    return 1;
}

Window::WindowType Widget::windowType() const { start_func
    return WINDOW_WIDGET;
}

char Widget::getShortcut() const { start_func
    return shortcut;
}

int Widget::getId() const { start_func
    return id;
}

void Widget::disable() { start_func
    if (!disabled) {
        disabled = 1;
        haveFocus = 0;
        setDirty(1);
    }
}

void Widget::enable() { start_func
    if (disabled) {
        disabled = 0;
        setDirty(1);
    }
}

Dialog* Widget::myParent() { start_func
    return dynamic_cast<Dialog*>(parent);
}

// Static widget

WStatic::WStatic(int sId, const string& sLabel) : Widget(sId, sLabel, NULL) { start_func
    // Calculate size (add one to width so we can properly draw disabled text)
    resize(fontWidth(label) + 1, fontHeight());
}

int WStatic::refuseAll() const { start_func
    if (disabled) return 1;
    return WStatic::REFUSE_STATIC;
}
    
int WStatic::event(int hasFocus, const SDL_Event* event) { start_func
    assert(event);
    assert(parent);
    
    switch (event->type) {
        case SDL_INPUTFOCUS:
            // Refuse focus
            if (event->user.code & 1) {
                myParent()->sendToBottom(this);
            }
            return 1;
    }    

    return 0;
}

void WStatic::changeText(const string& newLabel) { start_func
    setLabel(newLabel);

    // Recalculate size, but don't make smaller
    resize(max(width, fontWidth(label) + 1), height);

    setDirty();
}

void WStatic::load() { start_func
    // (static text does not store a value)
}

void WStatic::apply() { start_func
    // (static text does not store a value)
}

void WStatic::display(SDL_Surface* destSurface, Rect& toDisplay, const Rect& clipArea, int xOffset, int yOffset) { start_func
    assert(destSurface);

    if (visible) {
        // If dirty, redraw all
        if (dirty) {
            getRect(toDisplay);
            toDisplay.x += xOffset;
            toDisplay.y += yOffset;
            dirty = 0;
            intersectRects(toDisplay, clipArea);
        }
        
        // Anything to draw?
        if (toDisplay.w) {
            SDL_SetClipRect(destSurface, &toDisplay);

            xOffset += x;
            yOffset += y;
        
            // This widget makes no attempt at partial redraw other than the initial fill.
            SDL_FillRect(destSurface, &toDisplay, guiPacked[COLOR_FILL]);
        
            if (disabled) {
                drawText(label, guiRGB[COLOR_LIGHT1], xOffset + 1, yOffset + 1, destSurface);
                drawText(label, guiRGB[COLOR_DARK1], xOffset, yOffset, destSurface);
            }
            else {
                drawText(label, guiRGB[COLOR_TEXT], xOffset, yOffset, destSurface);
                if (underline) drawTextUnderline(underline, guiPacked[COLOR_DARK2], xOffset, yOffset, destSurface);                
            }
        }
    }
}

// Button widget

WButton::WButton(int bId, const string& bLabel, void* bSetting) : Widget(bId, bLabel, bSetting) { start_func
    action = Dialog::BUTTON_NOTHING;
    pressed = spacePressed = hover = 0;
    icon = NULL;
    isIcon = 0;
    command = NO_COMMAND;
}

WButton::WButton(int bId, SDL_Surface* iIconSurface, int iX, int iY, int iW, int iH, void* bSetting) : Widget(bId, blankString, bSetting) { start_func
    constructIcon(iIconSurface, iX, iY, iW, iH);

    action = Dialog::BUTTON_NOTHING;
    pressed = spacePressed = hover = 0;
    command = NO_COMMAND;
}

void WButton::constructIcon(SDL_Surface* iIconSurface, int iX, int iY, int iW, int iH) { start_func
    assert(iX >= 0);
    assert(iY >= 0);
    assert(iW > 0);
    assert(iH > 0);
    
    isIcon = 1;
    icon = iIconSurface;
    iconX = iX;
    iconY = iY;
    iconW = iW;
    iconH = iH;

    // Calculate size
    resize(iW + GUI_ICON_MARGIN * 2, iH + GUI_ICON_MARGIN * 2);
}

WButton::WButton(int bId, const string& bLabel, Dialog::ButtonAction bAction, int bCommand) : Widget(bId, bLabel, NULL) { start_func
    assert(bAction > Dialog::BUTTON_DEFAULT);
    assert(bAction <= Dialog::BUTTON_LASTACT);

    action = bAction;
    pressed = spacePressed = hover = 0;
    icon = NULL;
    isIcon = 0;
    command = bCommand;

    // Calculate size
    resize(fontWidth(label) + GUI_BUTTON_LEFTMARGIN + GUI_BUTTON_RIGHTMARGIN,
           fontHeight() + GUI_BUTTON_TOPMARGIN + GUI_BUTTON_BOTTOMMARGIN);
}

WButton::WButton(int bId, SDL_Surface* iIconSurface, int iX, int iY, int iW, int iH, Dialog::ButtonAction bAction, int bCommand) : Widget(bId, blankString, NULL) { start_func
    assert(bAction > Dialog::BUTTON_DEFAULT);
    assert(bAction <= Dialog::BUTTON_LASTACT);

    constructIcon(iIconSurface, iX, iY, iW, iH);

    action = bAction;
    pressed = spacePressed = hover = 0;
    command = bCommand;
}

void WButton::changeIcon(int iX, int iY) { start_func
    assert(iX >= 0);
    assert(iY >= 0);
    
    iconX = iX;
    iconY = iY;

    setDirty();
}

void WButton::changeText(const string& newLabel) { start_func
    setLabel(newLabel);

    // Recalculate size, but don't make smaller
    resize(max(width, fontWidth(label) + GUI_BUTTON_LEFTMARGIN + GUI_BUTTON_RIGHTMARGIN),
           fontHeight() + GUI_BUTTON_TOPMARGIN + GUI_BUTTON_BOTTOMMARGIN);

    setDirty();
}

Dialog::ButtonAction WButton::getAction() const { start_func
    return action;
}

void WButton::doAction() { start_func
    // Command?
    if (command != NO_COMMAND) {
        desktop->broadcastEvent(SDL_COMMAND, command);
        desktop->broadcastEvent(SDL_COMMAND, CMD_RELEASE);
    }
    
    // Handle button press
    Dialog::ButtonAction result = myParent()->verifyEntry(id, action);
    if (result == Dialog::BUTTON_DEFAULT) result = action;
    if (myParent()->doAction(result)) {
        desktop->setModalReturn(id);
    }
}

int WButton::event(int hasFocus, const SDL_Event* event) { start_func
    assert(event);
    assert(parent);

    switch (event->type) {
        case SDL_INPUTFOCUS:
            if (event->user.code & 1) {
                if (!haveFocus) {
                    haveFocus = 1;
                    setDirty();
                }
            }
            else {
                if ((haveFocus) || (spacePressed)) {
                    spacePressed = 0;
                    haveFocus = 0;
                    setDirty();
                }
            }
            return 1;
            
        case SDL_KEYDOWN:
            switch (combineKey(event->key.keysym.sym, event->key.keysym.mod)) {
                case SDLK_SPACE:
                    if (!spacePressed) {
                        spacePressed = 1;
                        setDirty();
                    }
                    return 1;
            }
            break;
            
        case SDL_KEYUP:
            // (no modifiers on key up)
            switch (event->key.keysym.sym) {
                case SDLK_SPACE:
                    if (spacePressed) {
                        spacePressed = 0;
                        doAction();
                        setDirty();
                        return 1;
                    }
                default:
                    break;
            }
            break;

        case SDL_MOUSEBUTTONUP:
            if (event->button.button == SDL_BUTTON_LEFT) {
                pressed = hover = 0;
                if ((event->button.x < width) && (event->button.y < height)) {
                    doAction();
                }
                setDirty();
                return 1;
            }
            break;

        case SDL_MOUSEBUTTONDBL:
        case SDL_MOUSEBUTTONDOWN:
            if (event->button.button == SDL_BUTTON_LEFT) {
                if ((event->button.x < width) && (event->button.y < height)) {
                    if ((!pressed) || (!hover)) {
                        pressed = hover = 1;
                        setDirty();
                    }
                }
                else {
                    if ((pressed) || (hover)) {
                        pressed = hover = 0;
                        setDirty();
                    }
                }
                return 1;
            }
            break;

        case SDL_MOUSEMOTION:
            if ((event->motion.x < width) && (event->motion.y < height)) {
                if (!hover) {
                    hover = 1;
                    setDirty();
                }
            }
            else {
                if (hover) {
                    hover = 0;
                    setDirty();
                }
            }
            if (event->motion.state & SDL_BUTTON_LMASK) {
                if (pressed != hover) {
                    pressed = hover;
                    setDirty();
                }
            }
            else {
                if (pressed) {
                    pressed = 0;
                    setDirty();
                }
            }
            return 1;
            
        case SDL_MOUSEFOCUS:
            if (event->user.code & 1) {
                if (!hover) {
                    hover = 1;
                    setDirty();
                }
            }
            else {
                if (hover) {
                    hover = 0;
                    setDirty();
                }
            }
            return 1;
    }
    
    return 0;
}

void WButton::load() { start_func
    // (buttons do not store a value)
    if ((pressed) || (spacePressed)) {
        pressed = 0;
        spacePressed = 0;
        setDirty();
    }
}

void WButton::apply() { start_func
    // (buttons do not store a value)
}

void WButton::iconDisplay(SDL_Surface* destSurface, int atX, int atY, int pressed) { start_func
    assert(destSurface);

    int offset = 0;
    int disableMargin;

    // This widget makes no attempt at partial redraw
    if (pressed) {
        // Non-gradient: drawRect(atX, atY, width, height, guiPacked[COLOR_FILL], destSurface);
        drawGradient(atX, atY, width, height, guiRGB[COLOR_BUTTONDOWN1], guiRGB[COLOR_BUTTONDOWN2], destSurface);
        drawGuiBoxInvert(atX, atY, width, height, 2, destSurface);
        disableMargin = 2;
        offset = 1;
    }
    else {
        drawGuiBoxInvert(atX, atY, width, height, 1, destSurface);
        drawGuiBox(atX + 1, atY + 1, width - 2, height - 2, 2, destSurface);
        // Non-gradient: nothing
        drawGradient(atX + 3, atY + 3, width - 6, height - 6, guiRGB[COLOR_BUTTONFILL1], guiRGB[COLOR_BUTTONFILL2], destSurface);
        disableMargin = 3;
    }
    if (icon) {
        blit(iconX, iconY, icon, atX + GUI_ICON_MARGIN + offset, atY + GUI_ICON_MARGIN + offset, destSurface, iconW, iconH);
        if (disabled) {
            // 75% opacity to disable
            drawSelectRect(atX + disableMargin, atY + disableMargin, width - disableMargin * 2, height - disableMargin * 2, guiPacked[COLOR_FILL], destSurface, 192);
        }
    }
    if (haveFocus) {
        drawFocusBox(atX + GUI_ICON_FOCUSMARGIN, atY + GUI_ICON_FOCUSMARGIN,
                     width - GUI_ICON_FOCUSMARGIN * 2,
                     height - GUI_ICON_FOCUSMARGIN * 2, destSurface);
    }
}

void WButton::display(SDL_Surface* destSurface, Rect& toDisplay, const Rect& clipArea, int xOffset, int yOffset) { start_func
    assert(destSurface);

    if (visible) {
        // If dirty, redraw all
        if (dirty) {
            getRect(toDisplay);
            toDisplay.x += xOffset;
            toDisplay.y += yOffset;
            dirty = 0;
            intersectRects(toDisplay, clipArea);
        }
        
        // Anything to draw?
        if (toDisplay.w) {
            SDL_SetClipRect(destSurface, &toDisplay);
        
            int offset = 0;
            xOffset += x;
            yOffset += y;
            
            // Icon?
            if (isIcon) {
                iconDisplay(destSurface, xOffset, yOffset, pressed || spacePressed);
            }
            else {
                // This widget makes no attempt at partial redraw other than the initial fill.
                if ((pressed) || (spacePressed)) {
                    // Non-gradient: SDL_FillRect(destSurface, &toDisplay, guiPacked[COLOR_FILL]);
                    drawGradient(xOffset, yOffset, width, height, guiRGB[COLOR_BUTTONDOWN1], guiRGB[COLOR_BUTTONDOWN2], destSurface);
                    drawGuiBoxInvert(xOffset, yOffset, width, height, 2, destSurface);
                    offset = 1;
                }
                else {
                    drawGuiBoxInvert(xOffset, yOffset, width, height, 1, destSurface);
                    drawGuiBox(xOffset + 1, yOffset + 1, width - 2, height - 2, 2, destSurface);
                    // Non-gradient: nothing
                    drawGradient(xOffset + 3, yOffset + 3, width - 6, height - 6, guiRGB[COLOR_BUTTONFILL1], guiRGB[COLOR_BUTTONFILL2], destSurface);
                }
                if (disabled) {
                    drawText(label, guiRGB[COLOR_LIGHT1], xOffset + GUI_BUTTON_LEFTMARGIN + 1 + offset, yOffset + GUI_BUTTON_TOPMARGIN + 1 + offset, destSurface);
                    drawText(label, guiRGB[COLOR_DARK1], xOffset + GUI_BUTTON_LEFTMARGIN + offset, yOffset + GUI_BUTTON_TOPMARGIN + offset, destSurface);
                }
                else {
                    drawText(label, guiRGB[COLOR_TEXT], xOffset + GUI_BUTTON_LEFTMARGIN + offset, yOffset + GUI_BUTTON_TOPMARGIN + offset, destSurface);
                    if (underline) drawTextUnderline(underline, guiPacked[COLOR_DARK2], xOffset + GUI_BUTTON_LEFTMARGIN + offset, yOffset + GUI_BUTTON_TOPMARGIN + offset, destSurface);
                }
                if (haveFocus) {
                    drawFocusBox(xOffset + GUI_BUTTON_HFOCUSMARGIN, yOffset + GUI_BUTTON_VFOCUSMARGIN,
                                 width - GUI_BUTTON_HFOCUSMARGIN * 2,
                                 height - GUI_BUTTON_VFOCUSMARGIN * 2, destSurface);
                }
            }
        }
    }
}

// Checkbox widget

int WCheckBox::checkboxSize = 0;
int WCheckBox::checkboxX = 0;
int WCheckBox::checkboxY = 0;
int WCheckBox::checkboxXText = 0;

WCheckBox::WCheckBox(int cId, const string& cLabel, int* cSetting, int cValue) : WButton(cId, cLabel, cSetting), affectControls() { start_func
    assert(cSetting);
    assert(cValue >= 0); // 0 is allowed for radio buttons

    value = cValue;
    currentStatus = 0;
    isRadio = 0;
    checkboxSize = fontHeight() * 4 / 5;
    checkboxX = GUI_CHECKBOX_PRECHECK + GUI_CHECKBOX_LEFTMARGIN;
    checkboxY = (fontHeight() - checkboxSize + 1) / 2 + GUI_CHECKBOX_TOPMARGIN;
    checkboxXText = GUI_CHECKBOX_POSTCHECK + GUI_CHECKBOX_LEFTMARGIN + checkboxSize;

    // Calculate size
    resize(fontWidth(label) + checkboxXText + GUI_CHECKBOX_RIGHTMARGIN,
           fontHeight() + GUI_CHECKBOX_TOPMARGIN + GUI_CHECKBOX_BOTTOMMARGIN);
}

WCheckBox::WCheckBox(int cId, SDL_Surface* iIconSurface, int iX, int iY, int iW, int iH, int* cSetting, int cValue) : WButton(cId, iIconSurface, iX, iY, iW, iH, cSetting), affectControls() { start_func
    assert(cSetting);
    assert(cValue >= 0); // 0 is allowed for radio buttons

    value = cValue;
    currentStatus = 0;
    isRadio = 0;
}

void WCheckBox::affectControl(int cId, int enable) { start_func
    affectControls[cId] = enable;
}

void WCheckBox::changeText(const string& newLabel) { start_func
    setLabel(newLabel);

    // Calculate size, but don't make smaller
    resize(max(width, fontWidth(label) + checkboxXText + GUI_CHECKBOX_RIGHTMARGIN),
           fontHeight() + GUI_CHECKBOX_TOPMARGIN + GUI_CHECKBOX_BOTTOMMARGIN);

    setDirty();
}

void WCheckBox::doAction() { start_func
    state(!currentStatus);
    setDirty();
}

void WCheckBox::load() { start_func
    state(*(int*)(setting) & value, 1);
    spacePressed = 0;
    pressed = 0;
    hover = 0;
    setDirty();
}

void WCheckBox::apply() { start_func
    *(int*)(setting) &= ~value;
    if (state()) *(int*)(setting) |= value;
}

int WCheckBox::state(int checked, int fromLoad) { start_func
    if (checked >= 0) {
        if (parent) {
            map<int, int>::iterator pos = affectControls.begin();
            for (; pos != affectControls.end(); ++pos) {
                Widget* affects = dynamic_cast<Dialog*>(parent)->findWidget((*pos).first);
                if (affects) {
                    int doEnable = checked;
                    if (!((*pos).second)) doEnable = !doEnable;
                    if (doEnable) affects->enable();
                    else affects->disable();
                }
            }
        }
        currentStatus = checked;
        setDirty();
        if ((!fromLoad) && (parent)) parent->childModified(this);
    }
    return currentStatus;
}

void WCheckBox::display(SDL_Surface* destSurface, Rect& toDisplay, const Rect& clipArea, int xOffset, int yOffset) { start_func
    assert(destSurface);

    if (visible) {
        // If dirty, redraw all
        if (dirty) {
            getRect(toDisplay);
            toDisplay.x += xOffset;
            toDisplay.y += yOffset;
            dirty = 0;
            intersectRects(toDisplay, clipArea);
        }
        
        // Anything to draw?
        if (toDisplay.w) {
            SDL_SetClipRect(destSurface, &toDisplay);

            int offset = 0;
            xOffset += x;
            yOffset += y;

            // Icon?
            if (isIcon) {
                iconDisplay(destSurface, xOffset, yOffset, pressed || spacePressed || state());
            }
            else {
                // This widget makes no attempt at partial redraw other than the initial fill.
                if ((pressed) || (spacePressed)) {
                    // Non-gradient: SDL_FillRect(destSurface, &toDisplay, guiPacked[COLOR_FILL]);
                    drawGradient(xOffset, yOffset, width, height, guiRGB[COLOR_BUTTONDOWN1], guiRGB[COLOR_BUTTONDOWN2], destSurface);
                    drawGuiBoxInvert(xOffset, yOffset, width, height, 1, destSurface);
                    ++offset;
                }
                else if (hover) {
                    drawGuiBox(xOffset, yOffset, width, height, 1, destSurface);
                    // Non-gradient: nothing
                    drawGradient(xOffset + 1, yOffset + 1, width - 2, height - 2, guiRGB[COLOR_BUTTONFILL1], guiRGB[COLOR_BUTTONFILL2], destSurface);
                }
                else {
                    SDL_FillRect(destSurface, &toDisplay, guiPacked[COLOR_FILL]);            
                }
                
                if (disabled) {
                    drawCheckbox(isRadio, state(), 1, xOffset + offset + checkboxX, yOffset + offset + checkboxY, checkboxSize, destSurface);
                    drawText(label, guiRGB[COLOR_LIGHT1], xOffset + checkboxXText + 1 + offset, yOffset + 1 + offset + GUI_CHECKBOX_TOPMARGIN, destSurface);
                    drawText(label, guiRGB[COLOR_DARK1], xOffset + checkboxXText + offset, yOffset + offset + GUI_CHECKBOX_TOPMARGIN, destSurface);
                }
                else {
                    drawCheckbox(isRadio, state(), 0, xOffset + offset + checkboxX, yOffset + offset + checkboxY, checkboxSize, destSurface);
                    drawText(label, guiRGB[COLOR_TEXT], xOffset + checkboxXText + offset, yOffset + offset + GUI_CHECKBOX_TOPMARGIN, destSurface);
                    if (underline) drawTextUnderline(underline, guiPacked[COLOR_DARK2], xOffset + checkboxXText + offset, yOffset + offset + GUI_CHECKBOX_TOPMARGIN, destSurface);
                }
            
                if (haveFocus) {
                    drawFocusBox(xOffset, yOffset, width, height, destSurface);
                }
            }
        }
    }
}

// Radio widget

WRadioButton::WRadioButton(int rId, const string& rLabel, int* rSetting, int rValue) : WCheckBox(rId, rLabel, rSetting, rValue) { start_func
    isRadio = 1;
}

WRadioButton::WRadioButton(int rId, SDL_Surface* iIconSurface, int iX, int iY, int iW, int iH, int* rSetting, int rValue) : WCheckBox(rId, iIconSurface, iX, iY, iW, iH, rSetting, rValue) { start_func
    isRadio = 1;
}

void WRadioButton::doAction() { start_func
    state(1);
}

void WRadioButton::load() { start_func
    state(*(int*)(setting) == value, 1);
    spacePressed = 0;
    pressed = 0;
    hover = 0;
    setDirty();
}

void WRadioButton::apply() { start_func
    if (state()) *(int*)(setting) = value;
}

int WRadioButton::state(int checked, int fromLoad) { start_func
    if ((checked >= 0) && (parent)) {
        map<int, int>::iterator pos = affectControls.begin();
        for (; pos != affectControls.end(); ++pos) {
            Widget* affects = dynamic_cast<Dialog*>(parent)->findWidget((*pos).first);
            if (affects) {
                int doEnable = checked;
                if (!((*pos).second)) doEnable = !doEnable;
                if (doEnable) affects->enable();
                else affects->disable();
            }
        }
    }
    if (checked > 0) {
        if (!currentStatus) setDirty();
        currentStatus = 1;
        
        // Unset all others of same id
        int pos = 0;
        Widget* widget;
        // (assignment intentional)
        while ( (widget = dynamic_cast<Widget*>(myParent()->findWindow(Window::WINDOW_WIDGET, pos++))) ) {
            if ((typeid(*widget) == typeid(*this)) && (widget->getId() == id)) {
                if (widget != this) dynamic_cast<WRadioButton*>(widget)->state(0, fromLoad);
            }
        }

        if ((!fromLoad) && (parent)) parent->childModified(this);
    }
    else if (checked == 0) {
        if (currentStatus) setDirty();
        currentStatus = 0;
        // Doesn't alert to modified if cleared, as another radio button handles that
    }
    return currentStatus;
}

// Textbox widget

const string WTextBox::wordChars("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");

WTextBox::WTextBox(int tId, string* tSetting, int tMaxSize, int tDisplayLength) : Widget(tId, blankString, tSetting) { start_func
    assert(tMaxSize >= 0);
    maxSize = tMaxSize;
    if ((maxSize == 0) || (maxSize > MAX_LINELENGTH)) maxSize = MAX_LINELENGTH;

    textX = GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN + GUI_TEXTBOX_LEFTPAD;
    textY = GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN;
    insertY = textY + 1;
    insertHeight = fontHeight() - 2;

    if (tDisplayLength == -1) {
        tDisplayLength = min(maxSize, (int)GUI_TEXTBOX_MAXSIZE);
        string standard(tDisplayLength, 'X');
        tDisplayLength = fontWidth(standard);
    }
    
    resize(tDisplayLength + (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN) * 2 + GUI_TEXTBOX_LEFTPAD + GUI_TEXTBOX_RIGHTPAD,
           fontHeight() + (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN) * 2);
}

// Needed due to virtual ~Widget() and our string member
WTextBox::~WTextBox() { start_func
}

void WTextBox::setDirtyAndBlink() { start_func
    insertBlink = 1;
    insertBlinkMs = SDL_GetTicks();
    setDirty();
}

Window::CommandSupport WTextBox::supportsCommand(int code) const { start_func
    switch (code) {
        case EDIT_COPY:
        case EDIT_CUT:
            if (selectionEnd == selectionBegin) return Window::COMMAND_DISABLE;
            return Window::COMMAND_ENABLE;
        
        case EDIT_PASTE:
            if (canConvertClipboard(CLIPBOARD_TEXT_LINE)) return Window::COMMAND_ENABLE;
            return Window::COMMAND_DISABLE;

        case EDIT_SELECTALL:
            if (currentValue.size())  return Window::COMMAND_ENABLE;
            return Window::COMMAND_DISABLE;
            
        case EDIT_UNDO:
            if (undoType == UNDO_NONE) return Window::COMMAND_DISABLE;
            return Window::COMMAND_ENABLE;
    }
    
    return Window::COMMAND_HIDE;
}

int WTextBox::event(int hasFocus, const SDL_Event* event) { start_func
    assert(event);
    assert(parent);

    string::size_type found;
    int drag = 0;
    Sint32 key;
    Sint16 scroll;
    string clipStorage;

    switch (event->type) {
        case SDL_MOUSEBUTTONDOWN:
            if ((event->button.button == SDL_BUTTON_LEFT) &&
                (event->button.y >= (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN)) &&
                (event->button.y < (height - GUI_TEXTBOX_BORDERTHICKNESS - GUI_TEXTBOX_INNERMARGIN)) &&
                (event->button.x >= (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN)) &&
                (event->button.x < (width - GUI_TEXTBOX_BORDERTHICKNESS - GUI_TEXTBOX_INNERMARGIN))) {
                insertionState(insertWhere(event->button.x), SDL_GetModState() & KMOD_SHIFT);
                return 1;
            }
            break;
    
        case SDL_MOUSEBUTTONDBL:
            if ((event->button.y >= (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN)) &&
                (event->button.y < (height - GUI_TEXTBOX_BORDERTHICKNESS - GUI_TEXTBOX_INNERMARGIN)) &&
                (event->button.x >= (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN)) &&
                (event->button.x < (width - GUI_TEXTBOX_BORDERTHICKNESS - GUI_TEXTBOX_INNERMARGIN))) {
                found = insertWhere(event->button.x) - 1;
                if (found < 0) found = 0;
                found = currentValue.find_last_not_of(wordChars, found);
                if (found == string::npos) found = 0;
                else ++found;
                insertionState(found, 0);
                found = currentValue.find_first_not_of(wordChars, insertPoint);
                if (found == string::npos) found = currentValue.size();
                insertionState(found, 1);
                return 1;
            }
            break;
    
        case SDL_MOUSEMOTION:
            if (event->motion.state & SDL_BUTTON_LMASK) {
                scroll = (Sint16)event->motion.x;
                insertionState(insertWhere(scroll), 1);
            }
            else if ((event->motion.y >= (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN)) &&
                (event->motion.y < (height - GUI_TEXTBOX_BORDERTHICKNESS - GUI_TEXTBOX_INNERMARGIN)) &&
                (event->motion.x >= (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN)) &&
                (event->motion.x < (width - GUI_TEXTBOX_BORDERTHICKNESS - GUI_TEXTBOX_INNERMARGIN))) {
                selectMouse(MOUSE_BEAM);
            }
            else {
                selectMouse(MOUSE_NORMAL);
            }
            return 1;

        case SDL_INPUTFOCUS:
            if (event->user.code & 1) {
                if (!haveFocus) {
                    haveFocus = 1;
                    setDirtyAndBlink();
                }
            }
            else {
                if (haveFocus) {
                    haveFocus = 0;
                    setDirty();
                }
            }
            return 1;            

        case SDL_SPECIAL:
            if ((event->user.code & SDL_IDLE) && (haveFocus)) {
                Uint32 ticks = SDL_GetTicks();
                // (catch wraparound)
                if (ticks < insertBlinkMs) insertBlinkMs = SDL_GetTicks();
                else if (ticks - insertBlinkMs > DELAY_CURSOR_BLINK) {
                    insertBlink = !insertBlink;
                    insertBlinkMs = SDL_GetTicks();
                    setDirty();
                }
                return 1;
            }
            break;
            
        case SDL_COMMAND:
            switch (event->user.code) {
                case EDIT_COPY:
                    copyText(clipStorage);
                    if (clipStorage.size()) clipboardCopy(clipStorage);
                    return 1;

                case EDIT_CUT:
                    copyText(clipStorage);
                    if (clipStorage.size()) {
                        saveUndo(UNDO_DELETE);
                        pasteText(blankString);
                        clipboardCopy(clipStorage);
                    }
                    return 1;

                case EDIT_PASTE:
                    if (canConvertClipboard(CLIPBOARD_TEXT_LINE)) {
                        saveUndo(UNDO_OTHER);
                        clipboardPasteTextLine(clipStorage);
                        pasteText(clipStorage);
                    }
                    return 1;

                case EDIT_SELECTALL:
                    selectAll();
                    return 1;
            
                case EDIT_UNDO:
                case EDIT_REDO:
                    undo();
                    return 1;
            }
            break;

        case SDL_KEYDOWN:
            if (event->key.keysym.mod & KMOD_SHIFT) {
                drag = 1; // We'll check this if we care about SHIFT
                key = combineKey(event->key.keysym.sym, event->key.keysym.mod & ~KMOD_SHIFT);
            }
            else {
                key = combineKey(event->key.keysym.sym, event->key.keysym.mod);
            }
        
            switch (key) {
                case SDLK_DELETE:
                    if (drag) break;
                    saveUndo(UNDO_DELETE);
                    keyDelete();
                    return 1;

                case SDLK_BACKSPACE:
                    if (drag) break;
                    saveUndo(UNDO_DELETE);
                    keyBackspace();
                    return 1;
                    
                case combineKey(SDLK_DELETE, KMOD_CTRL):
                    if (drag) break;
                    saveUndo(UNDO_DELETE);

                    // If no selection, delete to end of line
                    if (selectionBegin == selectionEnd) {
                        insertionState(currentValue.size(), 1);
                    }
                    
                    // Delete current selection
                    keyDelete();
                    return 1;
                    
                case SDLK_RIGHT:
                case SDLK_DOWN:
                    insertionState(insertPoint + 1, drag);
                    return 1;
                        
                case SDLK_LEFT:
                case SDLK_UP:
                    if (insertPoint > 0) insertionState(insertPoint - 1, drag);
                    return 1;

                case SDLK_HOME:
                    insertionState(0, drag);
                    return 1;

                case SDLK_END:
                    insertionState(currentValue.size(), drag);
                    return 1;

                case combineKey(SDLK_RIGHT, KMOD_CTRL):
                case combineKey(SDLK_DOWN, KMOD_CTRL):
                    found = currentValue.find_first_not_of(wordChars, insertPoint);
                    if (found == string::npos) found = currentValue.size();
                    found = currentValue.find_first_of(wordChars, found);
                    if (found == string::npos) found = currentValue.size();
                    insertionState(found, drag);
                    return 1;
                        
                case combineKey(SDLK_LEFT, KMOD_CTRL):
                case combineKey(SDLK_UP, KMOD_CTRL):
                    if (insertPoint <= 1) found = 0;
                    else {
                        found = currentValue.find_last_of(wordChars, insertPoint - 1);
                        if (found == string::npos) found = 0;
                        found = currentValue.find_last_not_of(wordChars, found);
                        if (found == string::npos) found = 0;
                        else if (found > 0) ++found;
                    }
                    insertionState(found, drag);
                    return 1;
                    
                default:
                    if ((event->key.keysym.unicode) && !(event->key.keysym.mod & KMOD_ALT)) {
                        if (event->key.keysym.unicode & 0xFF80) {
                            key = '?';
                        }
                        else {
                            key = event->key.keysym.unicode;
                            if (key < 32) break;
                        }
                        saveUndo(UNDO_TYPING);
                        string toInsert(1, key);
                        pasteText(toInsert);
                        return 1;
                    }
                    break;
            }
            break;
    }
    
    return 0;
}

void WTextBox::load() { start_func
    currentValue = *((string*)setting);
    if ((int)currentValue.length() > maxSize) currentValue = currentValue.substr(0, maxSize);

    undoValue = blankString;
    undoType = UNDO_NONE;
    
    insertPoint = 0;
    selectionBegin = 0;
    selectionEnd = 0;

    insertPointX = 0;
    selectionBeginX = 0;
    selectionEndX = 0;
    
    scrollPointX = 0;
    setDirty();
}

void WTextBox::apply() { start_func
    *((string*)setting) = currentValue;
}

void WTextBox::display(SDL_Surface* destSurface, Rect& toDisplay, const Rect& clipArea, int xOffset, int yOffset) { start_func
    assert(destSurface);

    if (visible) {
        // If dirty, redraw all
        if (dirty) {
            getRect(toDisplay);
            toDisplay.x += xOffset;
            toDisplay.y += yOffset;
            dirty = 0;
            intersectRects(toDisplay, clipArea);
        }
        
        // Anything to draw?
        if (toDisplay.w) {
            SDL_SetClipRect(destSurface, &toDisplay);
        
            xOffset += x;
            yOffset += y;
        
            // This widget makes no attempt at partial redraw.
            // (fill corners of inverted box first)
            drawRect(xOffset + width - GUI_TEXTBOX_BORDERTHICKNESS, yOffset, GUI_TEXTBOX_BORDERTHICKNESS, GUI_TEXTBOX_BORDERTHICKNESS, guiPacked[COLOR_FILL], destSurface);
            drawRect(xOffset, yOffset + height - GUI_TEXTBOX_BORDERTHICKNESS, GUI_TEXTBOX_BORDERTHICKNESS, GUI_TEXTBOX_BORDERTHICKNESS, guiPacked[COLOR_FILL], destSurface);
            drawGuiBoxInvert(xOffset, yOffset, width, height, GUI_TEXTBOX_BORDERTHICKNESS, destSurface);
        
            // Clear
            drawRect(xOffset + GUI_TEXTBOX_BORDERTHICKNESS, yOffset + GUI_TEXTBOX_BORDERTHICKNESS, width - 2 * GUI_TEXTBOX_BORDERTHICKNESS, height - 2 * GUI_TEXTBOX_BORDERTHICKNESS, guiPacked[disabled ? COLOR_FILL : COLOR_TEXTBOX], destSurface);
            
            // Text area
            Rect textClip = { xOffset + GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN,
                                  yOffset + GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN,
                                  width - 2 * (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN),
                                  height - 2 * (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN) };
            
            if (intersectRects(textClip, toDisplay)) {
                SDL_SetClipRect(destSurface, &textClip);
                     
                // Paint text
                if (disabled) {
                    drawText(currentValue, guiRGB[COLOR_LIGHT1], xOffset + textX + scrollPointX + 1, yOffset + textY + 1, destSurface);
                    drawText(currentValue, guiRGB[COLOR_DARK1], xOffset + textX + scrollPointX, yOffset + textY, destSurface);
                }
                else {
                    drawText(currentValue, guiRGB[COLOR_TEXT], xOffset + textX + scrollPointX, yOffset + textY, destSurface);
                }
                
                if (haveFocus) {
                    if (selectionBegin != selectionEnd) {
                        drawGradient(xOffset + textX + scrollPointX + selectionBeginX, yOffset + insertY,
                                     selectionEndX - selectionBeginX, insertHeight,
                                     guiRGB[COLOR_SELECTION1], guiRGB[COLOR_SELECTION2], destSurface);
                        // Redraw highlighted text inverted
                        drawText(currentValue.substr(selectionBegin, selectionEnd - selectionBegin), guiRGB[COLOR_TEXTBOX],
                                 xOffset + textX + scrollPointX + selectionBeginX, yOffset + textY, destSurface);
                    }
            
                    if (insertBlink) {
                        // Insertion point
                        drawRect(xOffset + textX + scrollPointX + insertPointX - 1, yOffset + insertY,
                                 GUI_TEXTBOX_INSERTPOINTWIDTH, insertHeight,
                                 guiPacked[COLOR_CURSOR], destSurface);
                    }
                }
            }
        }
    }
}

int WTextBox::allowChange(const string& oldValue, string* newValue, int oldCursorPos, int* newCursorPos) const { start_func
    return 1;
}

const string& WTextBox::state() const { start_func
    return currentValue;
}

const string& WTextBox::state(const string& newValue) { start_func
    // (sets dirty)
    selectAll();
    pasteText(newValue);
    return currentValue;
}

void WTextBox::saveUndo(UndoType type) { start_func
    // Any sequence of delete or typing is treated as one action
    if ((type == undoType) && ((type == UNDO_TYPING) || (type == UNDO_DELETE))) return;
    // Any sequence of typing after deleting is treated as one action
    if ((type == UNDO_TYPING) && (undoType == UNDO_DELETE)) {
        undoType = type;
        return;
    }
    undoPoint = insertPoint;
    undoValue = currentValue;
    undoType = type;
}

void WTextBox::undo() { start_func
    if (undoType == UNDO_NONE) return;
    undoType = UNDO_OTHER;
    // Swap undo and buffer
    swap(undoValue, currentValue);
    // Swap undo point and current point
    int newUndoPoint = insertPoint;
    insertionState(undoPoint);
    undoPoint = newUndoPoint;

    parent->childModified(this);
}

void WTextBox::selectAll() { start_func
    // (sets dirty)
    insertionState(0, 0);
    insertionState(currentValue.size(), 1);
}

void WTextBox::keyBackspace() { start_func
    if (selectionBegin == selectionEnd) {
        if (insertPoint == 0) return;
        insertionState(insertPoint - 1, 1);
    }
    // (sets dirty)
    pasteText(blankString);
}

void WTextBox::keyDelete() { start_func
    if (selectionBegin == selectionEnd) {
        if (insertPoint == (int)currentValue.size()) return;
        insertionState(insertPoint + 1, 1);
    }
    // (sets dirty)
    pasteText(blankString);
}

void WTextBox::copyText(string& buffer) const { start_func
    if (selectionEnd == selectionBegin) buffer = blankString;
    else buffer = currentValue.substr(selectionBegin, selectionEnd - selectionBegin);
}

int WTextBox::insertionState(int newPos, int drag) { start_func
    if (newPos >= 0) {
        // Clip on right
        if (newPos > (int)currentValue.size()) newPos = currentValue.size();
        
        // Drag?
        if (drag) {
            // No current selection?
            if (selectionBegin == selectionEnd) {
                selectionBegin = selectionEnd = insertPoint;
            }
            
            // If selection beginning matches insert point
            if (selectionBegin == insertPoint) {
                // Drag beginning point
                selectionBegin = insertPoint = newPos;
            }
            else {
                // Drag end point
                selectionEnd = insertPoint = newPos;
            }
            
            
            // No selection?
            if (selectionBegin == selectionEnd) {
                selectionBegin = selectionEnd = 0;
            }
            else {
                // Ensure in proper order
                if (selectionEnd < selectionBegin) {
                    swap(selectionBegin, selectionEnd);
                }
                
                // Determine point positions
                selectionBeginX = fontWidth(currentValue.substr(0, selectionBegin));
                selectionEndX = fontWidth(currentValue.substr(0, selectionEnd));
            }
        }
        else {
            // Reset selection
            selectionBegin = 0;
            selectionEnd = 0;
            selectionBeginX = 0;
            selectionEndX = 0;

            // Insertion point
            insertPoint = newPos;
        }

        // Insertion point position
        insertPointX = fontWidth(currentValue.substr(0, insertPoint));
        
        // Scroll?
        int rightMargin =  GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN + GUI_TEXTBOX_RIGHTPAD;
        int fullWidth = fontWidth(currentValue);
        if ((textX + scrollPointX + insertPointX) > (width - rightMargin - GUI_TEXTBOX_SCROLLAHEAD)) {
            scrollPointX = width - rightMargin - GUI_TEXTBOX_SCROLLAHEAD - insertPointX - textX;
        }
        if ((fullWidth + scrollPointX - width + rightMargin + textX) < 0) {
            scrollPointX = width - rightMargin - textX - fullWidth;
        }
        if ((scrollPointX + insertPointX) < GUI_TEXTBOX_SCROLLAHEAD) {
            scrollPointX = GUI_TEXTBOX_SCROLLAHEAD - insertPointX;
        }
        if (scrollPointX > 0) scrollPointX = 0;

        // Dirty
        setDirtyAndBlink();
    }
    
    return insertPoint;
}

void WTextBox::pasteText(const string& text) { start_func
    string newText = currentValue;
    int newPoint = insertPoint;

    // First, clear selection, if any
    if (selectionBegin != selectionEnd) {
        newText.erase(selectionBegin, selectionEnd - selectionBegin);
        newPoint = selectionBegin;
    }
    
    // Insert text
    newText.insert(newPoint, text);
    newPoint += text.size();
    
    // Validate
    if ((int)newText.size() <= maxSize) {
        if (allowChange(currentValue, &newText, insertPoint, &newPoint)) {
            currentValue = newText;
            selectionBegin = selectionEnd = 0;
        
            // Move insertion point (and sets dirty)
            insertionState(newPoint);
        }
    }

    parent->childModified(this);
}

int WTextBox::insertWhere(int x) const { start_func
    // Adjust by insertion point width for a bit of a fudge
    x -= textX + scrollPointX - GUI_TEXTBOX_INSERTPOINTWIDTH;
    for (int pos = currentValue.size(); pos > 0; --pos) {
        if (fontWidth(currentValue.substr(0, pos)) < x) return pos;
    }
    return 0;
}

void WTextBox::tabAction() { start_func
    selectAll();
}

// Numberbox widget

WNumberBox::WNumberBox(int tId, int* tSetting, int tMin, int tMax) : WTextBox(tId, NULL, 0) { start_func
    assert(tMin <= tMax);
    setting = tSetting;
    
    min = tMin;
    max = tMax;
    
    // Determine maximum size needed
    currentValue = intToStr(min);
    maxSize = currentValue.size();
    currentValue = intToStr(max);
    if ((int)currentValue.size() > maxSize) maxSize = currentValue.size();

    string standard(maxSize, 'X');
    resize(fontWidth(standard) + (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN) * 2 + GUI_TEXTBOX_LEFTPAD + GUI_TEXTBOX_RIGHTPAD,
           fontHeight() + (GUI_TEXTBOX_BORDERTHICKNESS + GUI_TEXTBOX_INNERMARGIN) * 2);
}

WNumberBox::~WNumberBox() { start_func
}

int WNumberBox::entryValid() const { start_func
    // Make sure numeric
    int valid = 1;
    int current;
    try {
        current = strToIntErr(currentValue);
    }
    catch (int) {
        valid = 0;
    }
    
    if ((current < min) || (current > max)) valid = 0;
    
    if (!valid) {
        string prompt = formatString("Please enter a value between %d and %d", min, max);
        guiErrorBox(prompt, errorTitleInvalidEntry);
        return 0;
    }
    
    return 1;
}

int WNumberBox::event(int hasFocus, const SDL_Event* event) { start_func
    assert(event);
    assert(parent);
    
    Sint32 key;

    switch (event->type) {
        case SDL_KEYDOWN:
            key = combineKey(event->key.keysym.sym, event->key.keysym.mod & ~KMOD_SHIFT);

            long current;
            long prevCurrent;
            int did = 0;

            // Make sure numeric
            try {
                current = strToIntErr(currentValue);
            }
            catch (int) {
                current = min;
            }
            
            prevCurrent = current;
    
            switch (key) {
                case SDLK_DOWN:
                    did = 1;
                    --current;
                    break;

                case SDLK_UP:
                    did = 1;
                    ++current;
                    break;

                case combineKey(SDLK_DOWN, KMOD_CTRL):
                case SDLK_PAGEDOWN:
                    did = 1;
                    current -= 10;
                    break;

                case combineKey(SDLK_UP, KMOD_CTRL):
                case SDLK_PAGEUP:
                    did = 1;
                    current += 10;
                    break;
            }
            
            if (did) {
                if (current > max) current = max;
                if (current < min) current = min;
                if (prevCurrent != current) {
                    saveUndo(UNDO_TYPING);
                    currentValue = intToStr(current);
                    parent->childModified(this);
                    insertionState(currentValue.size(), 0);
                }
                return 1;
            }
            
            break;
    }

    return WTextBox::event(hasFocus, event);
}

void WNumberBox::load() { start_func
    currentValue = intToStr(*((int*)setting));
    undoValue = blankString;
    undoType = UNDO_NONE;
    
    insertPoint = 0;
    selectionBegin = 0;
    selectionEnd = 0;

    insertPointX = 0;
    selectionBeginX = 0;
    selectionEndX = 0;
    
    scrollPointX = 0;
    setDirty();
}

void WNumberBox::apply() { start_func
    *((int*)setting) = strToInt(currentValue);
}

int WNumberBox::state() const { start_func
    return strToInt(currentValue);
}

int WNumberBox::state(int newValue) { start_func
    string newStr = intToStr(newValue);

    // (sets dirty)
    selectAll();
    pasteText(newStr);
    return strToInt(currentValue);
}

int WNumberBox::allowChange(const string& oldValue, string* newValue, int oldCursorPos, int* newCursorPos) const { start_func
    // Blank is specifically OK
    if (newValue->empty()) return 1;

    // + or - is OK if that range is enabled
    if ((newValue->size() == 1) && ((*newValue)[0] == '+') && (max > 0)) return 1;
    if ((newValue->size() == 1) && ((*newValue)[0] == '-') && (min < 0)) return 1;

    long current;
    
    // Make sure numeric
    try {
        current = strToIntErr(*newValue);
    }
    catch (int) {
        return 0;
    }
    
    // Any number from 1 to max is ok if max is positive
    if ((max > 0) && (current > 0) && (current <= max)) return 1;
    // Any number from -1 to min is ok if min is negative
    if ((min < 0) && (current < 0) && (current >= min)) return 1;
    // Zero is ok if zero is allowed
    if ((current == 0) && ((min <= 0) || (max >= 0))) return 1;
    
    return 0;
}

